﻿using pk3DS.Core;
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Forms;

namespace pk3DS.WinForms;

public partial class ShinyRate : Form
{
    public ShinyRate()
    {
        InitializeComponent();
        if (Main.ExeFSPath == null) { WinFormsUtil.Alert("No exeFS code to load."); Close(); }
        string[] files = Directory.GetFiles(Main.ExeFSPath);
        if (!File.Exists(files[0]) || !Path.GetFileNameWithoutExtension(files[0]).Contains("code")) { WinFormsUtil.Alert("No .code.bin detected."); Close(); }
        codebin = files[0];
        exefsData = File.ReadAllBytes(codebin);
        if (exefsData.Length % 0x200 != 0) { WinFormsUtil.Alert(".code.bin not decompressed. Aborting."); Close(); }

        // Load instruction set
        byte[] raw = Core.Properties.Resources.asm_mov;
        for (int i = 0; i < raw.Length; i += 4)
        {
            byte[] data = new byte[2];
            Array.Copy(raw, i + 2, data, 0, 2);
            InstructionList.Add(new Instruction(BitConverter.ToUInt16(raw, i), data));
        }

        // Fetch Offset
        byte[] pattern = [0x01, 0x50, 0x85, 0xE2, 0x05, 0x00, 0x50, 0xE1, 0xDE, 0xFF, 0xFF, 0xCA];
        offset = Util.IndexOfBytes(exefsData, pattern, 0, 0) - 4;
        if (offset < 0)
        {
            WinFormsUtil.Alert("Unable to find PID Generation routine.", "Closing.");
            Close();
        }
        if (exefsData[offset] != 0x23) // already patched
        {
            uint val = BitConverter.ToUInt16(exefsData, offset);
            var instruction = InstructionList.Find(z => z.ArgVal == val);
            if (instruction == null)
            {
                WinFormsUtil.Alert(".code.bin was modified externally.", "Existing value not loaded.");
            }
            else
            {
                WinFormsUtil.Alert(".code.bin was already patched for shiny rate.", "Loaded existing value.");
                NUD_Rerolls.Value = instruction.Value;
            }
            modified = true;
        }
        ChangeRerolls(null, null);

        CheckAlwaysShiny();
    }

    private readonly List<Instruction> InstructionList = [];
    private readonly bool modified;
    private readonly string codebin;
    private readonly int offset;
    private readonly byte[] exefsData;

    private class Instruction
    {
        public readonly int Value;
        private readonly byte[] Argument;
        public readonly ushort ArgVal;

        public Instruction(int val, byte[] arg)
        {
            Value = val;
            Argument = arg;
            ArgVal = BitConverter.ToUInt16(Argument, 0);
        }

        public byte[] Bytes
        {
            get
            {
                var bytes = new byte[] { 0, 0, 0xA0, 0xE3 };
                Argument.CopyTo(bytes, 0);
                return bytes;
            }
        }
    }

    private int alwaysIndex;

    private void CheckAlwaysShiny()
    {
        byte[] pattern = [0x00, 0x20, 0x22, 0xE0, 0x02, 0x30, 0x21, 0xE2, 0x03, 0x20, 0x92, 0xE1, 0x1C, 0x00, 0x00];
        int index = alwaysIndex = Util.IndexOfBytes(exefsData, pattern, 0, 0) + pattern.Length;

        if (index < 0)
        {
            CHK_EverythingShiny.Enabled = CHK_EverythingShiny.Visible = false;
            return;
        }

        bool original = exefsData[index] == 0x0A;
        bool always = exefsData[index] == 0xEA;

        if (!original && !always) // oh no
        {
            CHK_EverythingShiny.Enabled = CHK_EverythingShiny.Visible = false;
            return;
        }

        CHK_EverythingShiny.Checked = always;
    }

    private void B_Cancel_Click(object sender, EventArgs e) => Close();

    private void B_Save_Click(object sender, EventArgs e)
    {
        WriteCodePatch();
        if (CHK_EverythingShiny.Enabled)
            exefsData[alwaysIndex] = CHK_EverythingShiny.Checked ? (byte)0xEA : (byte)0x0A;
        File.WriteAllBytes(codebin, exefsData);
        Close();
    }

    private void ChangeRerolls(object sender, EventArgs e)
    {
        int count = (int)NUD_Rerolls.Value;
        const int bc = 4096;
        var pct = 1 - Math.Pow((float)(bc - 1) / bc, count);
        L_Overall.Text = $"~{pct:P}";
    }

    private void WriteCodePatch()
    {
        // Overwrite the "load input argument value for reroll count" so that it loads a constant value.
        // 23 00 D4 E5 is then replaced with the instruction MOV R0, $value
        // $value is the amount of PID rerolls to iterate for.

        int rerolls = (int)NUD_Rerolls.Value;
        if (rerolls > ushort.MaxValue)
            rerolls = ushort.MaxValue;
        // lazy precomputed table for MOV0 up to 9000, lol
        var instruction = InstructionList.Find(z => z.Value >= rerolls) ?? InstructionList[^1];
        byte[] data = instruction.Bytes;
        data.CopyTo(exefsData, offset);

        if (instruction.Value != rerolls)
            WinFormsUtil.Alert("Specified reroll count increased to the next highest supported value.", $"{rerolls} -> {instruction.Value}");
    }

    private void B_RestoreOriginal_Click(object sender, EventArgs e)
    {
        if (modified)
        {
            new byte[] { 0x23, 0x00, 0xD4, 0xE5 }.CopyTo(exefsData, offset);
            File.WriteAllBytes(codebin, exefsData);
        }
        Close();
    }

    private void ChangePercent(object sender, EventArgs e)
    {
        var pct = NUD_Rate.Value;
        const int bc = 4096;

        var inv = (int)Math.Log(1 - ((float)pct / 100), (float)(bc - 1) / bc);
        if (pct == 0)
            pct = 0.00001m; // arbitrary nonzero
        L_RerollCount.Text = $"Count: {inv:0} = 1:{(int)(1 / (pct / 100))}";
    }
}